﻿using System;
using System.Collections.Generic;
using System.Linq;
using GammaJul.ReSharper.GammaXaml.Highlightings.Xaml;
using GammaJul.ReSharper.GammaXaml.Psi;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using JetBrains.Annotations;
using JetBrains.ReSharper.Daemon.Stages;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;
using JetBrains.ReSharper.Psi.Xaml.Impl.Tree;
using JetBrains.ReSharper.Psi.Xaml.Tree;
using JetBrains.ReSharper.Psi.Xml.Tree;
using JetBrains.Util;

namespace GammaJul.ReSharper.GammaXaml.Analysis.Xaml {

	/// <summary>
	/// An analyzer that checks that :
	///  - Grid.Row, Grid.Column, Grid.RowSpan, Grid.ColumnSpan are within the row/column definitions bounds.
	///  - Grid.RowDefinitions and Grid.ColumnDefinitions have redundant defaults (one star-sized definition).
	/// </summary>
	public class GridDefinitionAnalysis : XamlAnalysis {
		
		
		#region Types

		private IXamlTypeCache _xamlTypeCache;
		private ITypeElement _gridType;
		private ITypeElement _rowDefinitionType;
		private ITypeElement _columnDefinitionType;

		[NotNull]
		private IXamlTypeCache XamlTypeCache {
			get { return _xamlTypeCache ?? (_xamlTypeCache = JetBrains.ReSharper.Psi.Xaml.Impl.Resolve.XamlTypeCache.GetCache(XamlFile)); }
		}

		[CanBeNull]
		private ITypeElement GridType {
			get { return _gridType ?? (_gridType = XamlTypeCache.Grid().GetTypeElement()); }
		}

		[CanBeNull]
		private ITypeElement RowDefinitionType {
			get { return _rowDefinitionType ?? (_rowDefinitionType = XamlTypeCache.RowDefinition().GetTypeElement()); }
		}

		[CanBeNull]
		private ITypeElement ColumnDefinitionType {
			get { return _columnDefinitionType ?? (_columnDefinitionType = XamlTypeCache.ColumnDefinition().GetTypeElement()); }
		}

		#endregion


		#region Grid Members

		private bool _gridMethodsPopulated;
		private IMethod _gridSetRowMethod;
		private IMethod _gridSetRowSpanMethod;
		private IMethod _gridSetColumnMethod;
		private IMethod _gridSetColumnSpanMethod;
		private IProperty _gridRowDefinitionsProperty;
		private IProperty _gridColumnDefinitionsProperty;

		/// <summary>
		/// Ensure that the four method attached setters fields and the two definitions setter are set.
		/// </summary>
		/// <remarks>
		/// Even after this method returns, the fields might still be <c>null</c> if some methods or properties haven't been found.
		/// This will only happens if the PresentationFramework/System.Windows reference is missing on the current project.
		/// </remarks>
		private void EnsureGridMembersPopulated() {
			if (_gridMethodsPopulated)
				return;

			_gridMethodsPopulated = true;

			ITypeElement gridType = GridType;
			if (gridType == null)
				return;

			IXamlTypeCache xamlTypeCache = XamlTypeCache;

			int count = 0;
			foreach (IMethod method in gridType.Methods) {
				Action<IMethod> methodSetter = GetMethodFieldSetter(method.ShortName);
				if (methodSetter != null && method.IsDpAttachedSetter(xamlTypeCache)) {
					methodSetter(method);
					// once we have the four attached property setters, exit since we have no interest in other methods
					if (++count == 4)
						break;
				}
			}

			count = 0;
			foreach (IProperty property in gridType.Properties) {
				Action<IProperty> propertySetter = GetPropertyFieldSetter(property.ShortName);
				if (propertySetter != null) {
					propertySetter(property);
					// once we have the two definition properties, exit since we have no interest in other properties
					if (++count == 2)
						break;
				}
			}
		}

		/// <summary>
		/// Returns an action that sets a field in the current class if the method name matches a grid definition attached setter.
		/// </summary>
		/// <param name="methodName">The name of the method.</param>
		/// <returns>An action that sets a field corresponding to <paramref name="methodName"/>, or <c>null</c> if the method name isn't recognized.</returns>
		[CanBeNull]
		private Action<IMethod> GetMethodFieldSetter([CanBeNull] string methodName) {
			switch (methodName) {
				case "SetRow":
					return m => _gridSetRowMethod = m;
				case "SetRowSpan":
					return m => _gridSetRowSpanMethod = m;
				case "SetColumn":
					return m => _gridSetColumnMethod = m;
				case "SetColumnSpan":
					return m => _gridSetColumnSpanMethod = m;
				default:
					return null;
			}
		}

		/// <summary>
		/// Returns an action that sets a field in the current class if the property name matches a grid definitions property.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		/// <returns>An action that sets a field corresponding to <paramref name="propertyName"/>, or <c>null</c> if the property name isn't recognized.</returns>
		[CanBeNull]
		private Action<IProperty> GetPropertyFieldSetter([CanBeNull] string propertyName) {
			switch (propertyName) {
				case "RowDefinitions":
					return p => _gridRowDefinitionsProperty = p;
				case "ColumnDefinitions":
					return p => _gridColumnDefinitionsProperty = p;
				default:
					return null;
			}
		}

		#endregion


		#region Row/ColumnDefinition Properties

		private IProperty _rowDefinitionHeightProperty;
		private IProperty _columnDefinitionWidthProperty;

		[CanBeNull]
		private IProperty RowDefinitionHeightProperty {
			get {
				if (_rowDefinitionHeightProperty == null) {
					ITypeElement rowDefType = RowDefinitionType;
					if (rowDefType != null)
						_rowDefinitionHeightProperty = rowDefType.Properties.FirstOrDefault(p => p.ShortName == "Height");
				}
				return _rowDefinitionHeightProperty;
			}
		}

		[CanBeNull]
		private IProperty ColumnDefinitionWidthProperty {
			get {
				if (_columnDefinitionWidthProperty == null) {
					ITypeElement columnDefType = ColumnDefinitionType;
					if (columnDefType != null)
						_columnDefinitionWidthProperty = columnDefType.Properties.FirstOrDefault(p => p.ShortName == "Width");
				}
				return _columnDefinitionWidthProperty;
			}
		}

		#endregion


		#region GridInfo

		private readonly Dictionary<IXamlObjectElement, GridInfo> _grids = new Dictionary<IXamlObjectElement, GridInfo>();
		
		/// <summary>
		/// Creates a <see cref="GridInfo"/> for a given grid.
		/// </summary>
		/// <param name="grid">The grid whose grid info will be created.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		/// <returns>A <see cref="GridInfo"/> for <paramref name="grid"/>.</returns>
		[NotNull]
		private GridInfo CreateGridInfo([NotNull] IXmlTagContainer grid, [NotNull] IHighlightingConsumer highlightings) {
			EnsureGridMembersPopulated();

			var gridInfo = new GridInfo();
			bool gotRowDefs = false;
			bool gotColDefs = false;

			foreach (var propElement in grid.GetTags<IXamlPropertyElement>()) {
				string xmlName = propElement.GetNameNode().XmlName;

				if (xmlName == "Grid.RowDefinitions" || xmlName.EndsWith("RowDefinitions", StringComparison.Ordinal)) {
					var memberReference = propElement.MemberReference;
					if (memberReference != null && Equals(memberReference.Resolve().DeclaredElement, _gridRowDefinitionsProperty)) {
						AddDefinitionsToList(gridInfo.RowDefinitionHeights, GridDefinitionType.Row, propElement,
							RowDefinitionHeightProperty, RowDefinitionType, highlightings);
					}
					gotRowDefs = true;
					if (gotColDefs)
						break;
				}

				if (xmlName == "Grid.ColumnDefinitions" || xmlName.EndsWith("ColumnDefinitions", StringComparison.Ordinal)) {
					var memberReference = propElement.MemberReference;
					if (memberReference != null && Equals(memberReference.Resolve().DeclaredElement, _gridColumnDefinitionsProperty)) {
						AddDefinitionsToList(gridInfo.ColumnDefinitionWidths, GridDefinitionType.Column, propElement,
							ColumnDefinitionWidthProperty, ColumnDefinitionType, highlightings);
					}
					gotColDefs = true;
					if (gotRowDefs)
						break;
				}
			}

			if (gridInfo.RowDefinitionHeights.Count == 0)
				gridInfo.RowDefinitionHeights.Add("*");
			if (gridInfo.ColumnDefinitionWidths.Count == 0)
				gridInfo.ColumnDefinitionWidths.Add("*");
			return gridInfo;
		}

		[NotNull]
		private static string GetDefinitionValue([CanBeNull] IPropertySetter setter) {
			if (setter == null)
				return "*";

			IXmlToken valueToken = setter.GetValueToken();
			if (valueToken == null)
				return "?";

			return valueToken.GetText();
		}

		/// <summary>
		/// Populates a list with row/column definitions from a row/column definitions setter node.
		/// </summary>
		/// <param name="values">The list to fill.</param>
		/// <param name="definitionType">The type of definition to populate.</param>
		/// <param name="definitionsSetter">A node corresponding to Grid.RowDefinitions/Grid.ColumnDefinitions.</param>
		/// <param name="sizeProperty">A property representing RowDefinition.HeightProperty or GridDefinition.WidthProperty.</param>
		/// <param name="defType">The RowDefinition/ColumnDefinition type.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void AddDefinitionsToList([NotNull] IList<string> values, GridDefinitionType definitionType,
			[NotNull] IPropertySetter definitionsSetter, [CanBeNull] ITypeMember sizeProperty, [CanBeNull] ITypeElement defType,
			[NotNull] IHighlightingConsumer highlightings) {
			if (sizeProperty == null || defType == null)
				return;

			foreach (var potentialDef in definitionsSetter.Children<IXamlObjectElement>()) {
				ITypeElement potentialDefType = potentialDef.TypeReference.ResolvedType();
				if (potentialDefType != null && potentialDefType.IsDescendantOf(defType)) {
					string defValue = GetDefinitionValue(potentialDef.GetPropertySetter(sizeProperty));
					values.Add(defValue);
				}
			}

			// Add an highlighting if the setters are redundant
			if (values.Count == 1 && IsStarSize(values[0]) && !definitionsSetter.ContainsErrorElement()) {
				var highlighting = new RedundantDefaultGridDefinitionCollectionSetterHighlighting(definitionsSetter, definitionType);
				highlightings.AddHighlighting(highlighting, XamlFile);
			}
		}

		private static bool IsStarSize([CanBeNull] string size) {
			if (String.IsNullOrEmpty(size))
				return false;
			
			int length = size.Length;
			if (size[length - 1] != '*')
				return false;

			if (length == 1)
				return true;

			double starSize;
			return Double.TryParse(size.Substring(0, length - 1), out starSize);
		}

		#endregion


		#region Processing

		public override bool InteriorShouldBeProcessed(ITreeNode element) {
			return (element is IXamlObjectElement || element is IXamlPropertyElement)
				&& base.InteriorShouldBeProcessed(element);
		}

		public override void ProcessBeforeInterior(ITreeNode element, IHighlightingConsumer highlightings) {
			var objectElement = element as IXamlObjectElement;
			if (objectElement != null)
				ProcessObjectElement(objectElement, highlightings);
			base.ProcessBeforeInterior(element, highlightings);
		}
		public override void ProcessAfterInterior(ITreeNode element, IHighlightingConsumer highlightings) {
			var xamlObjectElement = element as IXamlObjectElement;
			if (xamlObjectElement != null)
				_grids.Remove(xamlObjectElement);
			base.ProcessAfterInterior(element, highlightings);
		}

		/// <summary>
		/// Processes a XAML object element.
		/// </summary>
		/// <param name="objectElement">The object element to process.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void ProcessObjectElement([NotNull] IXamlObjectElement objectElement, [NotNull] IHighlightingConsumer highlightings) {

			// if we're on a Grid, adds it to the _grids set so that we can check for it quickly when we're on its children
			ITypeElement objectType = objectElement.TypeReference.ResolvedType();
			if (objectType != null && objectType.IsDescendantOf(GridType))
				ProcessGrid(objectElement, highlightings);

			// if we're on a Grid child, checks its Grid.Row/RowSpan/Column/ColumnSpan setters
			var parentElement = objectElement.Parent as IXamlObjectElement;
			GridInfo gridInfo;
			if (parentElement != null && _grids.TryGetValue(parentElement, out gridInfo))
				ProcessGridChild(objectElement, gridInfo, highlightings);
		}

		/// <summary>
		/// Processes a grid element.
		/// </summary>
		/// <param name="grid">The grid element.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void ProcessGrid([NotNull] IXamlObjectElement grid, [NotNull] IHighlightingConsumer highlightings) {
			_grids.Add(grid, CreateGridInfo(grid, highlightings));
		}

		/// <summary>
		/// Processes the child of a grid.
		/// </summary>
		/// <param name="gridChild">The child to process.</param>
		/// <param name="gridInfo">Grid information about the grid being the parent of <paramref name="gridChild"/>.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void ProcessGridChild([NotNull] IXmlTag gridChild, [NotNull] GridInfo gridInfo, [NotNull] IHighlightingConsumer highlightings) {
			EnsureGridMembersPopulated();

			IPropertySetter rowSetter = null, columnSetter = null, rowSpanSetter = null, columnSpanSetter = null;
			int count = 0;

			Func<IPropertySetter, bool> trySetSpecificSetters = setter =>
				TrySetSpecificSetterVariable(setter, _gridSetRowMethod, ref rowSetter)
				|| TrySetSpecificSetterVariable(setter, _gridSetColumnMethod, ref columnSetter)
				|| TrySetSpecificSetterVariable(setter, _gridSetRowSpanMethod, ref rowSpanSetter)
				|| TrySetSpecificSetterVariable(setter, _gridSetColumnSpanMethod, ref columnSpanSetter);

			// handle the attributes
			foreach (IPropertyAttribute attribute in gridChild.GetAttributes().OfType<IPropertyAttribute>()) {
				if (trySetSpecificSetters(attribute) && ++count == 4)
					break;
			}

			// if we haven't row, column, rowspan and columnspan, handle the tags
			if (count < 4) {
				foreach (IXamlPropertyElement element in gridChild.GetTags<IXamlPropertyElement>()) {
					if (trySetSpecificSetters(element) && ++count == 4)
						break;
				}
			}

			if (count == 0)
				return;

			ValidateDefAndSpan(rowSetter, rowSpanSetter, gridInfo.RowDefinitionHeights, GridDefinitionType.Row, highlightings);
			ValidateDefAndSpan(columnSetter, columnSpanSetter, gridInfo.ColumnDefinitionWidths, GridDefinitionType.Column, highlightings);
		}

		private void ValidateDefAndSpan([CanBeNull] IPropertySetter defSetter, [CanBeNull] IPropertySetter spanSetter, [NotNull] IList<string> defSizes, GridDefinitionType definitionType, [NotNull] IHighlightingConsumer highlightings) {
			int rowValue;
			IXmlToken valueToken;
			GridDefinitionValueOutOfRangeHighlighting highlighting;

			if (defSetter != null) {

				// Grid.Row/Column is not an int value, simply return: we don't have enough information to perform a correct analysis
				valueToken = defSetter.GetValueToken();
				if (valueToken == null || !Int32.TryParse(valueToken.GetText(), out rowValue))
					return;

				// Grid.Row/Column is an int value but outside the range of the row definitions, add a warning
				if (rowValue < 0 || rowValue >= defSizes.Count) {
					highlighting = new GridDefinitionValueOutOfRangeHighlighting(valueToken, new ReadOnlyList<string>(defSizes), definitionType);
					highlightings.AddHighlighting(highlighting, XamlFile);
				}
			}

			// no setter means that we have a Grid.Row/Column equivalent to 0
			else
				rowValue = 0;

			if (spanSetter == null)
				return;

			// Grid.RowSpan/ColumnSpan is not an int value, simply return: we don't have enough information to perform a correct analysis
			valueToken = spanSetter.GetValueToken();
			int rowSpanValue;
			if (valueToken == null || !Int32.TryParse(valueToken.GetText(), out rowSpanValue))
				return;

			// Grid.RowSpan is an int value but outside the range of the row definitions, add a warning
			if (rowSpanValue < 1 || rowValue + rowSpanValue > defSizes.Count) {
				highlighting = new GridSpanValueOutOfRangeHighlighting(valueToken, new ReadOnlyList<string>(defSizes), definitionType, rowValue);
				highlightings.AddHighlighting(highlighting, XamlFile);
			}
		}

		/// <summary>
		/// Tries to assign a variable corresponding to a property setter with a given setter, only if this setter
		/// corresponds to a given attached property setter method.
		/// </summary>
		/// <param name="setterToCheck">The XAML property setter to check.</param>
		/// <param name="expectedMethod">The method that must correspond to <paramref name="expectedMethod"/>.</param>
		/// <param name="varToSet">
		/// If it's not-<c>null</c>, will be untouched.
		/// If <paramref name="setterToCheck"/> matches <paramref name="expectedMethod"/>,
		/// this parameter will be set to <paramref name="setterToCheck"/> when the methods returns.
		/// </param>
		/// <returns><c>true</c> if <paramref name="varToSet"/> has been set to <paramref name="setterToCheck"/>; <c>false</c> otherwise.</returns>
		private static bool TrySetSpecificSetterVariable([NotNull] IPropertySetter setterToCheck, [CanBeNull] IMethod expectedMethod, ref IPropertySetter varToSet) {
			if (expectedMethod == null || varToSet != null)
				return false;

			IXmlIdentifier nameNode = setterToCheck.GetSetterNameNode();
			if (nameNode == null)
				return false;

			string name = nameNode.XmlName;

			string dpName = expectedMethod.GetExpectedDpName();
			if (name == dpName || name.EndsWith("." + dpName, StringComparison.Ordinal)) {
				ITypeMemberReference memberRef = setterToCheck.MemberReference;
				if (memberRef != null && Equals(memberRef.Resolve().DeclaredElement, expectedMethod)) {
					varToSet = setterToCheck;
					return true;
				}
			}

			return false;
		}
		
		#endregion


	}
}